package patch

import (
	"bytes"
	"errors"
	"fmt"
	"os"

	"github.com/fatih/color"
	"github.com/spf13/cobra"

	"github.com/go-dev-frame/sponge/cmd/sponge/commands/generate"
	"github.com/go-dev-frame/sponge/pkg/gofile"
	"github.com/go-dev-frame/sponge/pkg/replacer"
)

// GenerateDBInitCommand generate database initialization code
func GenerateDBInitCommand() *cobra.Command {
	var (
		moduleName string // go.mod module name
		dbDriver   string // database driver e.g. mysql, mongodb, postgresql, sqlite
		outPath    string // output directory
		targetFile = "internal/database/init.go"
	)

	cmd := &cobra.Command{
		Use:   "gen-db-init",
		Short: "Generate database initialization code",
		Long:  "Generate database initialization code.",
		Example: color.HiBlackString(`  # Generate mysql initialization code.
  sponge patch gen-db-init --module-name=yourModuleName --db-driver=mysql

  # Generate database initialization code, auto detect the database driver name, may be failed if the handler or service directory is not exists.
  sponge patch gen-db-init --out=./yourServerDir

  # Generate mysql initialization code, and specify the server directory, Note: code generation will be canceled when the latest generated file already exists.
  sponge patch gen-db-init --db-driver=mysql --out=./yourServerDir`),
		SilenceErrors: true,
		SilenceUsage:  true,
		RunE: func(cmd *cobra.Command, args []string) error {
			if outPath == "./" {
				outPath = "."
			}
			mdName, serverName, suitedMonoRepo := getNamesFromOutDir(outPath)
			if mdName != "" {
				moduleName = mdName
			} else if moduleName == "" {
				return fmt.Errorf(`required flag(s) "module-name" not set, use "sponge patch gen-db-init -h" for help`)
			}
			if suitedMonoRepo {
				if serverName == "" {
					return fmt.Errorf(`serverName is empty`)
				}
			}

			var isEmpty bool
			if outPath == "" {
				isEmpty = true
			} else {
				isEmpty = false
				initFile := outPath + "/" + targetFile
				if gofile.IsExists(initFile) {
					fmt.Printf("initialization code (%s) already exists, no need to generate it.\n", initFile)
					return nil
				}
			}

			if dbDriver == "" {
				// check handler and service directory db driver mark
				dbDriver = detectDbDriverName()
				if dbDriver == "" {
					fmt.Printf("no database driver found, ignored to generate initialization database code.\n")
					return nil
				}
			}

			g := &dbInitGenerator{
				moduleName: moduleName,
				dbDriver:   dbDriver,
				outPath:    outPath,

				serverName:     serverName,
				suitedMonoRepo: suitedMonoRepo,
			}
			var err error
			outPath, err = g.generateCode()
			if err != nil {
				return err
			}

			if isEmpty {
				fmt.Printf(`
using help:
  move the folder "internal" to your project code folder.

`)
			}
			fmt.Printf("generate \"%s\" initialization code successfully, out = %s\n", dbDriver, outPath)
			return nil
		},
	}

	cmd.Flags().StringVarP(&dbDriver, "db-driver", "k", "", "database driver, support mysql, mongodb, postgresql, sqlite")
	cmd.Flags().StringVarP(&moduleName, "module-name", "m", "", "module-name is the name of the module in the 'go.mod' file")
	cmd.Flags().StringVarP(&outPath, "out", "o", "", "output directory, default is ./mysql-init_<time>, "+
		"if you specify the directory where the web or microservice generated by sponge, the module-name flag can be ignored")

	return cmd
}

type dbInitGenerator struct {
	moduleName string
	dbDriver   string
	outPath    string

	serverName     string
	suitedMonoRepo bool
}

func (g *dbInitGenerator) generateCode() (string, error) {
	subTplName := "init_" + g.dbDriver
	r := generate.Replacers[generate.TplNameSponge]
	if r == nil {
		return "", errors.New("replacer is nil")
	}

	subDirs := []string{}
	selectFiles := map[string][]string{}
	err := generate.SetSelectFiles(g.dbDriver, selectFiles)
	if err != nil {
		return "", err
	}
	if len(selectFiles) == 0 {
		return "", errors.New("no files to generate")
	}

	r.SetSubDirsAndFiles(subDirs, getSubFiles(selectFiles)...)
	_ = r.SetOutputDir(g.outPath, subTplName)
	fields := g.addFields(r)
	r.SetReplacementFields(fields)
	if err := r.SaveFiles(); err != nil {
		return "", err
	}

	return r.GetOutputDir(), nil
}

func (g *dbInitGenerator) addFields(r replacer.Replacer) []replacer.Field {
	var fields []replacer.Field

	fields = append(fields, generate.DeleteCodeMark(r, generate.ModelInitDBFile, generate.StartMark, generate.EndMark)...)
	fields = append(fields, []replacer.Field{
		{
			Old:             "github.com/go-dev-frame/sponge/internal",
			New:             g.moduleName + "/internal",
			IsCaseSensitive: false,
		},
		{
			Old:             "github.com/go-dev-frame/sponge/configs",
			New:             g.moduleName + "/configs",
			IsCaseSensitive: false,
		},
		{
			Old: "init.go.mgo",
			New: "init.go",
		},
		{
			Old: "mongodb.go.mgo",
			New: "mongodb.go",
		},
		{ // replace the contents of the model/init.go file
			Old: generate.ModelInitDBFileMark,
			New: generate.GetInitDataBaseCode(g.dbDriver),
		},
	}...)

	if g.suitedMonoRepo {
		fs := generate.SubServerCodeFields(g.moduleName, g.serverName)
		fields = append(fields, fs...)
	}

	return fields
}

func getContentMark(dbDriver string) []byte {
	return []byte(generate.CurrentDbDriver(dbDriver))
}

func checkDbDriver(files []string) string {
	for _, file := range files {
		data, err := os.ReadFile(file)
		if err != nil {
			continue
		}

		if bytes.Contains(data, getContentMark(generate.DBDriverMysql)) ||
			bytes.Contains(data, getContentMark(generate.DBDriverTidb)) {
			return generate.DBDriverMysql
		}

		if bytes.Contains(data, getContentMark(generate.DBDriverMongodb)) {
			return generate.DBDriverMongodb
		}

		if bytes.Contains(data, getContentMark(generate.DBDriverPostgresql)) {
			return generate.DBDriverPostgresql
		}

		if bytes.Contains(data, getContentMark(generate.DBDriverSqlite)) {
			return generate.DBDriverSqlite
		}
	}
	return ""
}

func detectDbDriverName() string {
	var dbDriverName string
	files, _ := gofile.ListFiles("internal/handler", gofile.WithSuffix(".go"))
	if len(files) > 0 {
		dbDriverName = checkDbDriver(files)
		if dbDriverName != "" {
			return dbDriverName
		}
	}

	files, _ = gofile.ListFiles("internal/service", gofile.WithSuffix(".go"))
	if len(files) > 0 {
		dbDriverName = checkDbDriver(files)
	}
	return dbDriverName
}
